-module(geocode).

-behaviour(gen_server).

-include("../include/geocode.hrl").

-import(math, [acos/1, sin/1, cos/1, pi/0]).

-export([start_link/2,
         stop/0]).
-export([lookup/1,
         distance/2]).
-export([degrees_to_radians/1,
         radians_to_degrees/1]).
-export([init/1,
         handle_call/3,
         handle_cast/2,
         handle_info/2,
         terminate/2,
         code_change/3]).

-define(URL,     "http://maps.google.com/maps/geo").
-define(CACHE,   geocode_cache).
-define(TIMEOUT, timer:seconds(15)).

-record(state, {api_key, callback_ets}).

%% Starting
start_link(Key, Filename) ->
  gen_server:start_link({local, ?MODULE}, ?MODULE, [Key, Filename], []).

stop() ->
  gen_server:cast(?MODULE, stop).

%% Interface
lookup(Address) ->
  gen_server:call(?MODULE, {lookup, Address}, ?TIMEOUT).

%% Server
init([Key, Filename]) ->
  Options = [{type, set}, {file, Filename}, {keypos, index_of(address, record_info(fields, location))}],
  dets:open_file(?CACHE, Options),
  {ok, #state{api_key = Key, callback_ets = ets:new(callbacks, [set])}}.

handle_call({lookup, Address}, From, State) ->
  case do_lookup(to_canonical(Address), State#state.api_key) of
    {cache, Location} ->
      {reply, {ok, Location}, State};
    {callback, RequestID, Callback} ->
      ets:insert(State#state.callback_ets, {RequestID, From, Callback}),
      {noreply, State}
  end.

handle_cast(stop, State) ->
  dets:close(?CACHE),
  {stop, normal, State}.

handle_info({http, {_RequestID, {error, Error}}}, State) ->
  {stop, Error, State};
handle_info({http, {RequestID, HTTPResponse}}, State) ->
  [{RequestID, From, Callback}] = ets:lookup(State#state.callback_ets, RequestID),
  gen_server:reply(From, apply(Callback, [HTTPResponse])),
  {noreply, State};
handle_info(_Info, State) ->
  {noreply, State}.

terminate(_Reason, _State) ->
  ok.

code_change(_OldVsn, State, _Extra) ->
  {ok, State}.

%% Call
do_lookup(Address, Key) ->
  case dets:lookup(?CACHE, Address) of
    [Location] ->
      {cache, Location};
    [] ->
      URL         = ?URL ++ "?" ++ proplist_to_params([{q, Address}, {output, "csv"}, {key, Key}]),
      Headers     = [{"User-Agent", "erl-geocode"}],
      HTTPOptions = [],
      Options     = [{sync, false}, {body_format, binary}],
      Callback    = fun(HTTPResponse) ->
        case HTTPResponse of
          {{_, 200, _}, _, ResponseBinary} ->
            Text = binary_to_list(ResponseBinary),
            case regexp:split(Text, ",") of
              {ok, ["200", Acc, Lat, Lon]} ->
                Location = #location{lat      = list_to_float(Lat),
                                     lon      = list_to_float(Lon),
                                     address  = Address,
                                     accuracy = list_to_integer(Acc)},
                dets:insert(?CACHE, Location),
                {ok, Location};
              {ok, [Error | _]} ->
                {error, list_to_integer(Error)}
            end;
          {{_, Error, _}, _, _} ->
            {error, Error}
        end
      end,
      {ok, RequestID} = http:request(get, {URL, Headers}, HTTPOptions, Options),
      {callback, RequestID, Callback}
  end.

%% Canonical
to_canonical(Address) ->
  {ok, Canonical, _} = regexp:gsub(http_util:to_lower(Address), "[, ]+", " "),
  Canonical.

%% Distance
distance(Loc1, Loc2) ->
  #location{lat = Lat1, lon = Lon1} = degrees_to_radians(Loc1),
  #location{lat = Lat2, lon = Lon2} = degrees_to_radians(Loc2),
  Radius = 6378,
  acos(sin(Lat1) * sin(Lat2) + cos(Lat1) * cos(Lat2) * cos(Lon2 - Lon1)) * Radius.

%% Conversion
degrees_to_radians(Location = #location{lon = Lon, lat = Lat}) ->
  Location#location{lon = degrees_to_radians(Lon), lat = degrees_to_radians(Lat)};
degrees_to_radians(Degrees) ->
  Degrees * (pi() / 180).

radians_to_degrees(Location = #location{lon = Lon, lat = Lat}) ->
  Location#location{lon = radians_to_degrees(Lon), lat = radians_to_degrees(Lat)};
radians_to_degrees(Radians) ->
  Radians * (180 / pi()).

%% Utils
proplist_to_params(PropList) ->
  join("&", lists:map(fun({Key, Val}) ->
    yaws_api:url_encode(yaws:to_string(Key)) ++ "=" ++ yaws_api:url_encode(yaws:to_string(Val))
  end, PropList)).
  
join(Sep, List) ->
  lists:foldl(fun(A, "") -> A; (A, Acc) -> Acc ++ Sep ++ A end, "", List).

index_of(Elem, List) ->
  index_of_loop(Elem, List, 1).

index_of_loop(_Elem, [], _N) ->
  undefined;
index_of_loop(Elem, [Elem | _Rest], N) ->
  N;
index_of_loop(Elem, [_NotElem | Rest], N) ->
  index_of_loop(Elem, Rest, N+1).
